<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Three.js - Lots of Objects - Animated</title>
    <style>
    html, body {
        height: 100%;
        margin: 0;
        color: white;
    }
    #c {
        width: 100%;
        height: 100%;
        display: block;
    }
    #ui {
      position: absolute;
      left: 1em;
      top: 1em;
    }
    #ui>div {
      font-size: 20pt;
      padding: 1em;
      display: inline-block;
    }
    #ui>div.selected {
      color: red;
    }
    @media (max-width: 700px) {
      #ui>div {
        display: block;
        padding: .25em;
      }
    }
    </style>
  </head>
  <body>
    <canvas id="c"></canvas>
    <div id="ui"></div>
  </body>
<script type="importmap">
{
  "imports": {
    "three": "../../build/three.module.js",
    "three/addons/": "../../examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import TWEEN from 'three/addons/libs/tween.module.js';

class TweenManger {

	constructor() {

		this.numTweensRunning = 0;

	}
	_handleComplete() {

		-- this.numTweensRunning;
		console.assert( this.numTweensRunning >= 0 ); /* eslint no-console: off */

	}
	createTween( targetObject ) {

		const self = this;
		++ this.numTweensRunning;
		let userCompleteFn = () => {};

		// create a new tween and install our own onComplete callback
		const tween = new TWEEN.Tween( targetObject ).onComplete( function ( ...args ) {

			self._handleComplete();
			userCompleteFn.call( this, ...args );

		} );
		// replace the tween's onComplete function with our own
		// so we can call the user's callback if they supply one.
		tween.onComplete = ( fn ) => {

			userCompleteFn = fn;
			return tween;

		};

		return tween;

	}
	update() {

		TWEEN.update();
		return this.numTweensRunning > 0;

	}

}

function main() {

	const canvas = document.querySelector( '#c' );
	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
	const tweenManager = new TweenManger();

	const fov = 60;
	const aspect = 2; // the canvas default
	const near = 0.1;
	const far = 10;
	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
	camera.position.z = 2.5;

	const controls = new OrbitControls( camera, canvas );
	controls.enableDamping = true;
	controls.enablePan = false;
	controls.minDistance = 1.2;
	controls.maxDistance = 4;
	controls.update();

	const scene = new THREE.Scene();
	scene.background = new THREE.Color( 'black' );

	{

		const loader = new THREE.TextureLoader();
		const texture = loader.load( 'resources/images/world.jpg', render );
		texture.colorSpace = THREE.SRGBColorSpace;
		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
		const material = new THREE.MeshBasicMaterial( { map: texture } );
		scene.add( new THREE.Mesh( geometry, material ) );

	}

	async function loadFile( url ) {

		const req = await fetch( url );
		return req.text();

	}

	function parseData( text ) {

		const data = [];
		const settings = { data };
		let max;
		let min;
		// split into lines
		text.split( '\n' ).forEach( ( line ) => {

			// split the line by whitespace
			const parts = line.trim().split( /\s+/ );
			if ( parts.length === 2 ) {

				// only 2 parts, must be a key/value pair
				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );

			} else if ( parts.length > 2 ) {

				// more than 2 parts, must be data
				const values = parts.map( ( v ) => {

					const value = parseFloat( v );
					if ( value === settings.NODATA_value ) {

						return undefined;

					}

					max = Math.max( max === undefined ? value : max, value );
					min = Math.min( min === undefined ? value : min, value );
					return value;

				} );
				data.push( values );

			}

		} );
		return Object.assign( settings, { min, max } );

	}

	function addBoxes( file, hueRange ) {

		const { min, max, data } = file;
		const range = max - min;

		// these helpers will make it easy to position the boxes
		// We can rotate the lon helper on its Y axis to the longitude
		const lonHelper = new THREE.Object3D();
		scene.add( lonHelper );
		// We rotate the latHelper on its X axis to the latitude
		const latHelper = new THREE.Object3D();
		lonHelper.add( latHelper );
		// The position helper moves the object to the edge of the sphere
		const positionHelper = new THREE.Object3D();
		positionHelper.position.z = 1;
		latHelper.add( positionHelper );
		// Used to move the center of the cube so it scales from the position Z axis
		const originHelper = new THREE.Object3D();
		originHelper.position.z = 0.5;
		positionHelper.add( originHelper );

		const color = new THREE.Color();

		const lonFudge = Math.PI * .5;
		const latFudge = Math.PI * - 0.135;
		const geometries = [];
		data.forEach( ( row, latNdx ) => {

			row.forEach( ( value, lonNdx ) => {

				if ( value === undefined ) {

					return;

				}

				const amount = ( value - min ) / range;

				const boxWidth = 1;
				const boxHeight = 1;
				const boxDepth = 1;
				const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );

				// adjust the helpers to point to the latitude and longitude
				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;

				// use the world matrix of the origin helper to
				// position this geometry
				positionHelper.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
				originHelper.updateWorldMatrix( true, false );
				geometry.applyMatrix4( originHelper.matrixWorld );

				// compute a color
				const hue = THREE.MathUtils.lerp( ...hueRange, amount );
				const saturation = 1;
				const lightness = THREE.MathUtils.lerp( 0.4, 1.0, amount );
				color.setHSL( hue, saturation, lightness );
				// get the colors as an array of values from 0 to 255
				const rgb = color.toArray().map( v => v * 255 );

				// make an array to store colors for each vertex
				const numVerts = geometry.getAttribute( 'position' ).count;
				const itemSize = 3; // r, g, b
				const colors = new Uint8Array( itemSize * numVerts );

				// copy the color into the colors array for each vertex
				colors.forEach( ( v, ndx ) => {

					colors[ ndx ] = rgb[ ndx % 3 ];

				} );

				const normalized = true;
				const colorAttrib = new THREE.BufferAttribute( colors, itemSize, normalized );
				geometry.setAttribute( 'color', colorAttrib );

				geometries.push( geometry );

			} );

		} );

		const mergedGeometry = BufferGeometryUtils.mergeGeometries(
			geometries, false );
		const material = new THREE.MeshBasicMaterial( {
			vertexColors: true,
			transparent: true,
			opacity: 0,
		} );
		const mesh = new THREE.Mesh( mergedGeometry, material );
		scene.add( mesh );
		return mesh;

	}

	async function loadData( info ) {

		const text = await loadFile( info.url );
		info.file = parseData( text );

	}

	async function loadAll() {

		const fileInfos = [
			{ name: 'men', hueRange: [ 0.7, 0.3 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
			{ name: 'women', hueRange: [ 0.9, 1.1 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
		];

		await Promise.all( fileInfos.map( loadData ) );

		function mapValues( data, fn ) {

			return data.map( ( row, rowNdx ) => {

				return row.map( ( value, colNdx ) => {

					return fn( value, rowNdx, colNdx );

				} );

			} );

		}

		function makeDiffFile( baseFile, otherFile, compareFn ) {

			let min;
			let max;
			const baseData = baseFile.data;
			const otherData = otherFile.data;
			const data = mapValues( baseData, ( base, rowNdx, colNdx ) => {

				const other = otherData[ rowNdx ][ colNdx ];
				if ( base === undefined || other === undefined ) {

					return undefined;

				}

				const value = compareFn( base, other );
				min = Math.min( min === undefined ? value : min, value );
				max = Math.max( max === undefined ? value : max, value );
				return value;

			} );
			// make a copy of baseFile and replace min, max, and data
			// with the new data
			return { ...baseFile, min, max, data };

		}

		// generate a new set of data
		{

			const menInfo = fileInfos[ 0 ];
			const womenInfo = fileInfos[ 1 ];
			const menFile = menInfo.file;
			const womenFile = womenInfo.file;

			function amountGreaterThan( a, b ) {

				return Math.max( a - b, 0 );

			}

			fileInfos.push( {
				name: '>50%men',
				hueRange: [ 0.6, 1.1 ],
				file: makeDiffFile( menFile, womenFile, ( men, women ) => {

					return amountGreaterThan( men, women );

				} ),
			} );
			fileInfos.push( {
				name: '>50% women',
				hueRange: [ 0.0, 0.4 ],
				file: makeDiffFile( womenFile, menFile, ( women, men ) => {

					return amountGreaterThan( women, men );

				} ),
			} );

		}

		function showFileInfo( fileInfos, fileInfo ) {

			fileInfos.forEach( ( info ) => {

				const durationInMs = 1000;
				const visible = fileInfo === info;
				//        const scale = visible ? 1 : 0.1;
				const opacity = visible ? 1 : 0;
				info.elem.className = visible ? 'selected' : '';
				info.root.visible = visible || info.root.material.opacity > 0;
				tweenManager.createTween( info.root.material )
					.to( { opacity }, durationInMs )
					.start()
					.onComplete( () => {

						info.root.visible = visible;

					} );
				//        tweenManager.createTween(info.root.material)
				//          .to({depthWrite: visible}, 0)
				//          .delay(durationInMs * .5)
				//          .start();
				//        tweenManager.createTween(info.root)
				//          .to({visible}, 0)
				//          .delay(durationInMs)
				//          .start();
				//        tweenManager.createTween(info.root.scale)
				//          .to({x: scale, y: scale, z: scale}, durationInMs)
				//          .start();

			} );
			requestRenderIfNotRequested();

		}

		const uiElem = document.querySelector( '#ui' );
		fileInfos.forEach( ( info ) => {

			const boxes = addBoxes( info.file, info.hueRange );
			info.root = boxes;
			//      boxes.scale.set(0.1, 0.1, 0.1);
			const div = document.createElement( 'div' );
			info.elem = div;
			div.textContent = info.name;
			uiElem.appendChild( div );
			function show() {

				showFileInfo( fileInfos, info );

			}

			div.addEventListener( 'mouseover', show );
			div.addEventListener( 'touchstart', show );

		} );
		// show the first set of data
		showFileInfo( fileInfos, fileInfos[ 0 ] );

	}

	loadAll();

	function resizeRendererToDisplaySize( renderer ) {

		const canvas = renderer.domElement;
		const width = canvas.clientWidth;
		const height = canvas.clientHeight;
		const needResize = canvas.width !== width || canvas.height !== height;
		if ( needResize ) {

			renderer.setSize( width, height, false );

		}

		return needResize;

	}

	let renderRequested = false;

	function render() {

		renderRequested = undefined;

		if ( resizeRendererToDisplaySize( renderer ) ) {

			const canvas = renderer.domElement;
			camera.aspect = canvas.clientWidth / canvas.clientHeight;
			camera.updateProjectionMatrix();

		}

		if ( tweenManager.update() ) {

			requestRenderIfNotRequested();

		}

		controls.update();
		renderer.render( scene, camera );

	}

	render();

	function requestRenderIfNotRequested() {

		if ( ! renderRequested ) {

			renderRequested = true;
			requestAnimationFrame( render );

		}

	}

	controls.addEventListener( 'change', requestRenderIfNotRequested );
	window.addEventListener( 'resize', requestRenderIfNotRequested );

}

main();
</script>
</html>
